-
Notifications
You must be signed in to change notification settings - Fork 1.1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Pages List performance improvements #22070
Conversation
c90bf98
to
1a32c7a
Compare
📲 You can test the changes from this Pull Request in WordPress Alpha by scanning the QR code below to install the corresponding build.
|
📲 You can test the changes from this Pull Request in Jetpack Alpha by scanning the QR code below to install the corresponding build.
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Leaving some notes before moving on with trying this on device.
func fetchAllPages(statuses: [BasePost.Status], in blogID: TaggedManagedObjectID<Blog>) -> Task<[TaggedManagedObjectID<Page>], Swift.Error> { | ||
func fetchAllPages(statuses: [BasePost.Status], authorUserID: NSNumber? = nil, in blogID: TaggedManagedObjectID<Blog>) -> Task<[TaggedManagedObjectID<Page>], Swift.Error> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nitpick: The Swift API Guidelines recommend to locate parameters with default values at the end of the list:
Prefer to locate parameters with defaults toward the end of the parameter list. Parameters without defaults are usually more essential to the semantics of a method, and provide a stable initial pattern of use where methods are invoked.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good catch. I used default parameter value quite freely in a few similar functions in this class. I want to keep the blog parameter as the last parameter (or the first), and rest of the parameters (which act as post/pages filters) don't really have an sensible order. But these functions are added one by one across many PRs, it's probably worth looking into these function holistically later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sounds good
struct PageData { | ||
var postID: NSNumber? | ||
var parentID: NSNumber? | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Always like a container type for information like this one.
Do the properties need to be mutable?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe no? I used var
out of habit without any extra thoughts. However, it's used as a constant though: let pageData: PageData
.
@@ -475,85 +475,80 @@ class AbstractPostListViewController: UIViewController, | |||
return .any | |||
} | |||
|
|||
func syncHelper(_ syncHelper: WPContentSyncHelper, syncContentWithUserInteraction userInteraction: Bool, success: ((_ hasMore: Bool) -> ())?, failure: ((_ error: NSError) -> ())?) { | |||
@MainActor | |||
func syncPosts(isFirstPage: Bool) async throws -> ([AbstractPost], Bool) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
From the function signature, I couldn't understand what that Bool
in the returned tuple represents. I only figured it out by looking at the usage, where it's assigned to hasMore
.
Have you considered defining a struct
to model that information?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Addressed in 5981dcf. I used a named tuple instead of a struct.
refreshResults() | ||
} | ||
|
||
/// Build page hierachy in background, which should not take long (less than 2 seconds for 6000+ pages). |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Show off 😄
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
😅 Taking 2 seconds to sort 6000 items is not really fast though...It's much faster than what we have at the moment, but still not fast enough. I'm being lazy here by putting them into a background thread. The comment here is mainly pointing out, the sorting isn't fast enough to take place in the main thread, but is fast enough to put in the background thread without worrying it keep going forever.
guard let (posts, hasMore) = try await self?.syncPosts(isFirstPage: false) else { return } | ||
|
||
guard let self else { return } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would it make sense to swap these? The end result would be the same, return if self is not available, but I feel it's odd to access self?
and check let self
afterwards. 🤷♂️
guard let (posts, hasMore) = try await self?.syncPosts(isFirstPage: false) else { return } | |
guard let self else { return } | |
guard let self else { return } | |
let (posts, hasMore) = try await self.syncPosts(isFirstPage: false) |
I didn't try this code, maybe I'm missing something and in fact it's necessary to do guard
twice.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This guard let self
is similar to how we do the same check in a completion block. In reality, self
should never be nil when calling syncPosts
, but could be nil if user has exit the screen before the sycnPosts
returns. I have added a comment in 19307cc
let pageTree = PageTree() | ||
pageTree.add(pages) | ||
let list = pageTree.hierarchyList() | ||
let list = (try? PageTree.hierarchyList(of: pages)) ?? [] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was going to suggest the following, which I feel would make the point at which the test might have failed.
let list = (try? PageTree.hierarchyList(of: pages)) ?? [] | |
let list = try XCTUnwrap(try? PageTree.hierarchyList(of: pages))) |
But I checked the docs and I guess it's not possible to throw
from the block given to measure
😞
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep, that's why I added an assertion below, to make sure we are measuring success cases, not failure cases.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Worked great on my iPhone 15 Pro.
I took videos for comparison, then remembered the site to test this edge case is a private one 🥷
Anyways, on the same device, the 23.7 beta load two pages in the list then nothing else happens in the UI. In this build, the rest of the pages appear after a second or so.
Great work 👏
Generated by 🚫 dangerJS |
Enabled auto-merge so this can land in |
Fixes #21813. Also, in general, this PR makes it's possible to load a site with a large number of pages (300+).
Issue
Currently Pages List performs very poorly on a site with a large number of pages. A pretty extreme case is #21813. The app may become unresponsive in 5 minutes or so, when about 6000 pages are loaded.
If you have a test site with 300 pages, you can open that site in the app and go to Pages screen and the app will become unresponsive very quickly.
If you don't have a test site with that many pages, you can use FG as the test site, with a little bit of code change: adding another "and" condition to this if statement:
&& loadedPosts.count < 500
. The number 500 is the max of pages you want to the app to fetch. You can tweak this number to test the Pages List's performance against certain number of pages.Changes
There are a few issues involved:
The first two issues are addressed by the
PostRepository.fetchAllPages
function. And the last issues is addressed by the linked PR #22056. This PR basically uses those new implementations to swap out the current problematic ones.That means, this PR should not bring any UI changes to the app, it only addresses performance issues.
Note
It'd be much easier to review this PR commit by commit.
Further considerations
In other discussions, the general consensus is it's bad to load all pages automatically, instead of page by page as user scrolls through the list (like Posts List). @kean mentioned we could change Pages List to match WP.com, where the pages are displayed in a hierarchical order if there are certain number in total (say 600), but they are displayed page by page (like Posts List) if the total amount exceed this threshold.
I agree with that idea, but did not implement it in this PR, because I don't want this PR to get too large. I'm not sure if I'd have time to implement it though, because I have other project lined up (dropping Alamofire from WordPressKit).
Test Instructions
Verify #21813 is fixed. Use FG to test Pages List performance.
This PR also adds a cancellation mechanism to the Pages List, where the Pages List stops loading more pages when user taps the "Back" button. You can verify this behaviour by using Xcode's "Network Activity Report" in the Debug panel: You should see periodic spikes in the Network Activity when the app are on FG's pages list and see no more spikes when exits the pages list screen.
Regression Notes
Potential unintended areas of impact
None.
What I did to test those areas of impact (or what existing automated tests I relied on)
Test using FG.
What automated tests I added (or what prevented me from doing so)
None.
PR submission checklist:
RELEASE-NOTES.txt
if necessary.UI Changes testing checklist: N/A